<?php
/**
*
* CHANGELOG
*
*
*
*
* Uses Singleton
*/

class Yz_HTTP_Request
    extends Yz_Class
    implements Yz_HTTP_Request_Interface
{

    protected static $instance;

    // "environment"
    protected $acceptLanguage; // Accept-Language
    protected $date; // READONLY
    protected $host; // READONLY
    protected $hostname; // READONLY
    protected $ifModifiedSince; // READONLY
    protected $isAJAX; // READONLY
    protected $isPOST; // READONLY
    protected $lang; // from HTTP Accept-Language ?
    protected $method; // READONLY GET|POST
    protected $path; // the full 'path' of the request
    protected $query; // QUERY_STRING
    protected $remoteAddr; // READONLY
    protected $scriptName; // READONLY
    protected $webPrefix; // READONLY
    protected $webRoot; // READONLY

    protected $arguments;

    /**
     *  Yz_Web_Request::getInstance()
     *
     *  @return Yz_Web_Request
     */
    public static function getInstance()
    {
        debug();
        $i =& self::$instance;
        if( null === $i ) {
            $i = new static();
        }
        return $i;
    } // getInstance


    /**
     * Yz_Web_Request::toString()
     *
     * @return string
     * @todo something more meaningful
     */
    public function __toString()
    {
        debug();

        return $this->path;
    } // toString


    /**
     * fetch an argument from the GET parameters
     *
     * @param string $key
     *  @param mixed default: the type of returned argument must match the type of default
     * @return mixed
     */
    public function fetchArgument( $key, $default=null )
    {
        debug();
        return $this->fetchArgumentValue( $key, $default, $_GET );
    } // fetchArgument



    /**
     * fetch an argument from the POST parameters
     *
     * @param string $key
     *  @param mixed default: the type of returned argument must match the type of default
     * @return mixed
     */
    public function fetchPOSTArgument( $key, $default=null )
    {
        return $this->fetchArgumentValue( $key, $default, $_POST );
    } // fetchPOSTArgument


    /**
     * fetch a custom Header
     *
     * @param string $header
     * @return string|null
     */
    //TODO: verify header contents
    public function fetchHeader( $header )
    {
        debug();

        // throws exception
        $header = Yz_String::testNotEmpty( $header, 'HTTP header name' );

        $header = 'HTTP_' . strtoupper( strtr($header, '-', '_') );
        return $this->_fetchHeader($header);
    } // fetchHeader()

    /**
     * Yz_Web_Request::getAcceptLanguage()
     *
     * @return array?|false
     */
    public function getAcceptLanguage()
    {
        $a =& $this->acceptLanguage;
        if( null === $a ) {
            $a = $this->_fetchHeader('HTTP_ACCEPT_LANGUAGE');
            /* TODO handle: obtain correct values */
            if (null === $a) {
                $a = false;
            }
        }
        return $a;
    } // getAcceptLanguage

    /**
     * get the Host header
     *
     * @return string
     */
    public function getHost()
    {
        $host =& $this->host;
        if ( null === $host ) {
            $host = $this->_fetchHeader('HTTP_HOST');
            if (null === $host) {
                $host = "";
            } else {

                // may be an international host name, too
                $host = strtolower($host);

                debug("Host: {$host}");
                // contains invalid characters
                if( ! preg_match( '~[a-z0-9][a-z0-9\.-]*(?:\:\d+)~', $host ) ) {
                    debug("Host name invalid: {$host}");
                    $host = "";
                }
            }
        }
        return $host;
    }


    /**
     * get the Hostname of the running HTTP server
     *
     * @return string
     */
    public function getHostname()
    {
        $host =& $this->hostname;
        if ( null === $host ) {
            $host = $this->getHost();

            if ( "" === $host ) {
                // this must always be set
                $host = $_SERVER['SERVER_NAME'];
                if ( ! empty($_SERVER['SERVER_PORT']) && '80' !== (string)$_SERVER['SERVER_PORT'] ) {
                    $host .= ":" . $_SERVER['SERVER_PORT'];
                }
            }

        }
        return $host;
    } // getHostname

    /**
     * Yz_Web_Request::getIfModifiedSince()
     *
     * @return timestamp|false
     */
    public function getIfModifiedSince()
    {
        $ifm =& $this->ifModifiedSince;
        if( null === $ifm ) {
            $h = $this->_fetchHeader('HTTP_IF_MODIFIED_SINCE');
            if ( null === $h ) {
                $ifm = false;
            } else {
                $ifm = (int)strtotime($h);
            }
        }
        return $ifm;
    } // getIfModifiedSince

    /**
     * Yz_Web_Request::getMethod()
     *
     * @return string
     */
    public function getMethod()
    {
        if( null === $this->method ) {
            $this->method = $_SERVER['REQUEST_METHOD'];
        }
        return $this->method;
    } // getMethod

    /**
     * Yz_Web_Request::getPath()
     *
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    } // getPath


    /**
     * Yz_Web_Request::getQuery()
     *
     * @return string
     */
    public function getQuery()
    {
        if( null === $this->query ) {
            $this->query = $_SERVER['QUERY_STRING'];
        }
        return $this->query;
    } // getMethod


    /**
     * Yz_Web_Request::getRemoteAddr()
     *
     * @return string IP
     */
    public function getRemoteAddr()
    {
        if( null === $this->remoteAddr ) {
            $this->remoteAddr = $_SERVER['REMOTE_ADDR'];
        }
        return $this->remoteAddr;
    } // getRemoteAddr


    /** get the yz cms objects' links prefix
     *  @return string always ending with /
     */
    public function getWebPrefix()
    {
        return $this->webPrefix;
    }


    /** get the web path to yz root directory
     *  @return string always ending with /
     */
    public function getWebRoot()
    {
        return $this->webRoot;
    }


    /** whether request was made via AJAX
     *
     *  @return bool
     */
    public function isAJAX()
    {
        if( null === $this->isAJAX ) {

            // treat request as AJAX if X_REQUESTED_WITH is set to any value
            $this->isAJAX = ( null !== $this->_fetchHeader( 'HTTP_X_REQUESTED_WITH' ) );
        }
        return $this->isAJAX;
    } // isAJAX


    /** whether request method is POST
     *
     *  @return bool
     */
    public function isPOST()
    {
        if( null === $this->isPOST ) {
            $this->isPOST = ( 'POST' === $this->getMethod() );
        }
        return $this->isPOST;
    } // isPOST


    /**
     * Yz_HTTP_Request::getArguments()
     *
     * @return Yz_Hash arguments, pruned
     */
    public function getArguments()
    {
        debug();
        $args = $_GET;
        // TODO prune: remove empty values
        // rename arrays or remove them at all
        return new Yz_Hash($args);
    } // getArguments()

    /**
     * Yz_HTTP_Request::getPOSTArguments()
     *
     * @return Yz_Hash arguments, pruned
     */
    public function getPOSTArguments()
    {
        debug();
        $args = $_POST;
        // TODO prune: remove empty values
        // rename arrays or remove them at all
        return new Yz_Hash($args);
    } // getPOSTArguments()


    /*****************************
     *      Protected methods    *
     *****************************/


    /**
     * Yz_Web_Request::__construct()
     */
    protected function __construct()
    {
        debug();

        $this->path = $this->identifyRequestPath();

    } // __construct


    /** validate argument name as non-empty string
     *  @param string $key
     *  @return string
     *  @throw InvalidValueType
     */
    protected function adjustKey( $key )
    {
        $key = Yz_String::testNotEmpty( $key, 'request argument name' );
        return $key;
    } // adjustKey


    /** Fetch a value from arguments
     *      if default is not null, the type of returned value will match the type of default
     *  @param string $key
     *  @param mixed default
     *  @param array $args
     *  @throw InvalidValueType
     *  @return mixed
     */
    protected function fetchArgumentValue( $key, $default, $args )
    {
        // throws exception
        $key = $this->adjustKey( $key );

        if (isset($args[$key])) {
            $arg = $args[$key];
            if (null === $default) {
                return $arg;
            }
            if (is_bool($default)) {
                return is_string($arg) ? Yz_String::isTrue($arg) : $default;
            }
            if (is_string($default)) {
                return is_string($arg) ? $arg : $default;
            }
            if (is_array($default)) {
                return is_array($arg) ? $arg : $default;
            }
        }
        return $default;
    }

    /** fetch a header and ensure that it is a string (return null if no such header)
     *  @param string $header
     *  @return string|null
     *  @throw Yz_HTTP_Exception_InvalidHeader
     */
    protected function _fetchHeader($header)
    {
        $h = null;
        if (array_key_exists($header, $_SERVER)) {
            $h = $_SERVER[$header];
            if (! is_string($h)) {
                throw Yz_HTTP_Exception_InvalidHeader::_new(array(
                    'header' => $header,
                ));
            }
        }
        return $h;
    }


    /** determine the usable path of the request (path related to the Yz system), and
     *      determine the web root/prefix
     *  @return string
     */
    protected function identifyRequestPath()
    {
        if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
            throw new Yz_HTTP_Exception_BadRequest();
        }

        $path = strtok( $_SERVER['REQUEST_URI'], '?' );

        debug("REQUEST_URI path: '$path'");

        // ###  if rewrite is not active, path may be derived from PATH_INFO:
        // ###  /index.php/path
        $scriptName = $_SERVER['PHP_SELF'];

        // the engine's web-root is the directory path to its entry script
        $webRoot = dirname($scriptName);
        "/" === $webRoot or $webRoot .= '/';

        $webPrefix = $scriptName . '/';
        if ( 0 === strncasecmp( $webPrefix, $path . "/", strlen($webPrefix) )) {

            // the full script name is present in the request path => rewrite apparently off
            debug("rewrite apparently OFF");
            $path = substr( $path, strlen($webPrefix) );
        } else {
            debug("rewrite apparently ON");
            $webPrefix = $webRoot;
            if ("/" !== $webRoot) {

                // remove the web-root from effective path
                if ( 0 === strncasecmp( $webRoot, $path . "/", strlen($webRoot) )) {
                    $path = substr( $path, strlen($webRoot) );
                }
            }
        }

        debug("ScriptName: '$scriptName', webRoot: '$webRoot', prefix: '$webPrefix'");
        $this->webRoot = $webRoot;
        $this->webPrefix = $webPrefix;

        return $path;
    }

} // class Yz_HTTP_Request

